/*            Copyright(c) 2006-2008 Dave Sexton           *
 * http://www.codeplex.com/DocProject                      *
 * http://www.codeplex.com/DocProject/Project/License.aspx *
 ***********************************************************/
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Configuration.Install;
using System.Diagnostics;
using System.IO;
using System.Windows.Forms;
using System.Xml;

namespace DaveSexton.DocProject.InstallActions
{
	[RunInstaller(true)]
	public sealed partial class CustomActions : Installer
	{
		#region Private / Protected
		private const string installPathEnvironmentVariable = "DocProjectPath";
		private const string legacyInstallPathEnvironmentVariable = "DocProjectBuildPath";
		private const string addinAssemblyName = "DaveSexton.DocProject.dll";
		private const string addinFileName = "DocProject.Addin";
		private const string externalUIConfigurationFileName = "DocProject.exe.config";
		private const string assemblyBindingConfigXmlNamespace = "urn:schemas-microsoft-com:asm.v1";

		private static readonly IList<Template> webTemplates = new List<Template>(new Template[] {
			new Template("CSharpDocSiteTemplate.zip", "DocProject", TemplateType.VisualCSharp), 
			new Template("VBDocSiteTemplate.zip", "DocProject", TemplateType.VisualBasic)
		});

		private static readonly IList<Template> csTemplates = new List<Template>(new Template[] {
			new Template("CSharpDocProjectTemplate.zip", "DocProject", TemplateType.VisualCSharp)
		});

		private static readonly IList<Template> vbTemplates = new List<Template>(new Template[] {
			new Template("VBDocProjectTemplate.zip", "DocProject", TemplateType.VisualBasic)
		});

		private static readonly IList<Template> allTemplates = CombineTemplateLists();

		private IEnumerable<VisualStudioContent> Contents
		{
			get
			{
				// NOTE: Do not use the C# iterator pattern (yield return) since the value of this property will be serialized and must retain its exact state.
				// Using the iterator pattern will cause all yielded objects to be recreated during uninstallation when the reference to IEnumerable<T> is deserialized, 
				// and the new objects will not have the same state that was created during installation (such as the IsValid flag being false, for example, until the new objects are initialized themselves).
				
				if (allVSContent == null)
				{
					allVSContent = new List<VisualStudioContent>();

					if (InstallIntoVS2005)
					{
						// Visual Studio Standard or higher
						allVSContent.Add(new VisualStudioContent(allTemplates, addinFileName, "devenv.exe", "VisualStudio", "8.0", "2005"));

						// Visual Studio Express Editions
						allVSContent.Add(new VisualStudioContent(csTemplates, "VCSExpress.exe", "VCSExpress", "8.0", "2005"));
						allVSContent.Add(new VisualStudioContent(vbTemplates, "VBExpress.exe", "VBExpress", "8.0", "2005"));
					}

					if (InstallIntoVS2008)
					{
						// Visual Studio Standard or higher
						allVSContent.Add(new VisualStudioContent(allTemplates, addinFileName, "devenv.exe", "VisualStudio", "9.0", "2008"));

						// Visual Studio Express Editions
						allVSContent.Add(new VisualStudioContent(csTemplates, "VCSExpress.exe", "VCSExpress", "9.0", "2008"));
						allVSContent.Add(new VisualStudioContent(vbTemplates, "VBExpress.exe", "VBExpress", "9.0", "2008"));
					}
				}

				return allVSContent;
			}
		}

		private bool AllUsers
		{
			get
			{
				if (allUsers == null)
					allUsers = "1".Equals(Context.Parameters["everyone"], StringComparison.Ordinal);

				return allUsers.Value;
			}
		}

		private string Target
		{
			get
			{
				if (target == null)
				{
					target = Context.Parameters["target"];

					if (string.IsNullOrEmpty(target))
						throw new InvalidOperationException(Resources.Errors.InstallNoTarget);
				}

				return target;
			}
		}

		private string TargetBin
		{
			get
			{
				return Path.Combine(Target, "bin");
			}
		}

		private string DocProjectAddInAssembly
		{
			get
			{
				return Path.Combine(TargetBin, addinAssemblyName);
			}
		}

		private string ExternalUIConfigurationFile
		{
			get
			{
				return Path.Combine(TargetBin, externalUIConfigurationFileName);
			}
		}

		private bool InstallIntoVS2005
		{
			get
			{
				return "1".Equals(Context.Parameters["vs2005"], StringComparison.Ordinal);
			}
		}

		private bool InstallIntoVS2008
		{
			get
			{
				return "1".Equals(Context.Parameters["vs2008"], StringComparison.Ordinal);
			}
		}

		private List<VisualStudioContent> allVSContent;
		private string target;
		private bool? allUsers;
		#endregion

		#region Constructors
		/// <summary>
		/// Constructs a new instance of the <see cref="CustomActions" /> class.
		/// </summary>
		public CustomActions()
		{
			InitializeComponent();

			EventLogInstaller eventLogInstaller = new EventLogInstaller();
			eventLogInstaller.Source = "DocProject";
			eventLogInstaller.Log = "Application";

			Installers.Add(eventLogInstaller);

			AppDomain.CurrentDomain.AssemblyResolve += (o, e) =>
				{
					System.Reflection.AssemblyName name = new System.Reflection.AssemblyName(e.Name);

					if (name.Name == "DaveSexton.DocProject")
						return System.Reflection.Assembly.LoadFrom(DocProjectAddInAssembly);
					else
						return null;
				};
		}
		#endregion

		#region Methods
		private static List<Template> CombineTemplateLists()
		{
			List<Template> templates = new List<Template>(webTemplates);
			templates.AddRange(csTemplates);
			templates.AddRange(vbTemplates);

			return templates;
		}

		private static string SerializeContents(IEnumerable<VisualStudioContent> contents)
		{
			// installer uses XML serialization but binary serialization is required by the VisualStudoContent class
			System.Runtime.Serialization.Formatters.Binary.BinaryFormatter formatter = new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter();

			using (MemoryStream stream = new MemoryStream())
			{
				formatter.Serialize(stream, new List<VisualStudioContent>(contents).ToArray());

				return Convert.ToBase64String(stream.ToArray());
			}
		}

		private static IEnumerable<VisualStudioContent> DeserializeContents(string contents)
		{
			System.Runtime.Serialization.Formatters.Binary.BinaryFormatter formatter = new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter();

			using (MemoryStream stream = new MemoryStream(Convert.FromBase64String(contents)))
			{
				return new List<VisualStudioContent>(formatter.Deserialize(stream) as VisualStudioContent[]).AsReadOnly();
			}
		}

		public static XmlNamespaceManager GetXmlNamespaceManagerForAssemblyBindingConfig(XmlDocument document)
		{
			XmlNamespaceManager ns = new XmlNamespaceManager(document.NameTable);
			ns.AddNamespace("net", assemblyBindingConfigXmlNamespace);
			return ns;
		}

		private ContentInstaller CreateInstaller()
		{
			return new ContentInstaller(Target, addinAssemblyName, AllUsers);
		}

		private void InstallEnvironmentVariables()
		{
			// NOTE: In testing, a reboot was NOT required when setting environment variables in code but
			// a reboot IS required when environment variables are written directly to the registry
			// in the Setup and Deployment project (Installer), so this code is being used instead

			Environment.SetEnvironmentVariable(installPathEnvironmentVariable, Target, 
				(AllUsers) ? EnvironmentVariableTarget.Machine : EnvironmentVariableTarget.User);

			Environment.SetEnvironmentVariable(legacyInstallPathEnvironmentVariable, TargetBin,
				(AllUsers) ? EnvironmentVariableTarget.Machine : EnvironmentVariableTarget.User);
		}

		private void UninstallEnvironmentVariables()
		{
			Environment.SetEnvironmentVariable(installPathEnvironmentVariable, null, 
				(AllUsers) ? EnvironmentVariableTarget.Machine : EnvironmentVariableTarget.User);

			Environment.SetEnvironmentVariable(legacyInstallPathEnvironmentVariable, null,
				(AllUsers) ? EnvironmentVariableTarget.Machine : EnvironmentVariableTarget.User);
		}

		private void UpdateExternalUIConfiguration()
		{
			string file = ExternalUIConfigurationFile;

			XmlDocument document = new XmlDocument();
			document.Load(file);

			XmlNamespaceManager ns = GetXmlNamespaceManagerForAssemblyBindingConfig(document);

			XmlNode assemblyBinding = GetAssemblyBindingNode(document, true);

			bool hasVCChanges = false, hasMSBuildChanges = false;

			Microsoft.Win32.RegistryKey msBuildKey = Microsoft.Win32.Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\MSBuild\3.5");
			bool ensureMSBuildBindings = msBuildKey != null;

			if (ensureMSBuildBindings)
			{
				hasMSBuildChanges |= AddBindingRedirect(ns, document, assemblyBinding, "Microsoft.Build.Framework", "B03F5F7F11D50A3A", "neutral", "0.0.0.0-3.5.0.0", "3.5.0.0");
				hasMSBuildChanges |= AddBindingRedirect(ns, document, assemblyBinding, "Microsoft.Build.Engine", "B03F5F7F11D50A3A", "neutral", "0.0.0.0-3.5.0.0", "3.5.0.0");
			}
			else
			{
				hasMSBuildChanges |= RemoveBindingRedirect(ns, assemblyBinding, "Microsoft.Build.Framework");
				hasMSBuildChanges |= RemoveBindingRedirect(ns, assemblyBinding, "Microsoft.Build.Engine");
			}

			Microsoft.Win32.RegistryKey key = Microsoft.Win32.Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\VisualStudio\9.0");
			bool ensureVCBindng = key != null && !string.IsNullOrEmpty(key.GetValue("InstallDir", null) as string);

			if (ensureVCBindng)
				hasVCChanges |= AddBindingRedirect(ns, document, assemblyBinding, "Microsoft.VisualStudio.VCProjectEngine", "b03f5f7f11d50a3a", "neutral", "8.0.0.0", "9.0.0.0");
			else
				hasVCChanges |= RemoveBindingRedirect(ns, assemblyBinding, "Microsoft.VisualStudio.VCProjectEngine");

			if (hasVCChanges || hasMSBuildChanges)
			{
				document.Save(file);

				if (hasVCChanges)
					Log.Warn("DocProject External UI configuration file updated.\r\n\r\n{0} VCProjectEngine assembly binding redirect from version 8.0.0.0 to 9.0.0.0.\r\n\r\nFile: {1}",
						(ensureVCBindng) ? "Added" : "Removed", file);

				if (hasMSBuildChanges)
					Log.Warn("DocProject External UI configuration file updated.\r\n\r\n{0} MSBuild assembly binding redirects for 3.5.0.0.\r\n\r\nFile: {1}",
						(ensureMSBuildBindings) ? "Added" : "Removed", file);
			}
		}

		internal static XmlNode GetAssemblyBindingNode(XmlDocument document, bool createIfMissing)
		{
			if (document.DocumentElement == null)
				document.AppendChild(document.CreateElement("configuration"));

			XmlNode runtime = document.DocumentElement["runtime"];

			if (runtime == null)
			{
				if (!createIfMissing)
					return null;

				runtime = document.DocumentElement.AppendChild(document.CreateElement("runtime"));
			}

			XmlNode assemblyBinding = runtime["assemblyBinding", assemblyBindingConfigXmlNamespace];

			if (assemblyBinding == null)
			{
				if (!createIfMissing)
					return null;

				assemblyBinding = runtime.AppendChild(document.CreateElement("assemblyBinding", assemblyBindingConfigXmlNamespace));
			}

			return assemblyBinding;
		}

		internal static bool AddBindingRedirect(XmlNamespaceManager ns, XmlDocument document, XmlNode assemblyBinding, 
			string assemblyName, string publicKeyToken, string culture, string oldVersion, string newVersion)
		{
			XmlNode dependentAssembly = assemblyBinding.SelectSingleNode("net:dependentAssembly[net:assemblyIdentity/@name='" + assemblyName + "']", ns);
			
			if (dependentAssembly == null)
			{
				dependentAssembly = assemblyBinding.AppendChild(document.CreateElement("dependentAssembly", assemblyBindingConfigXmlNamespace));

				XmlNode assemblyIdentity = dependentAssembly.AppendChild(document.CreateElement("assemblyIdentity", assemblyBindingConfigXmlNamespace));
				assemblyIdentity.Attributes.Append(document.CreateAttribute("name")).Value = assemblyName;
				assemblyIdentity.Attributes.Append(document.CreateAttribute("publicKeyToken")).Value = publicKeyToken;
				assemblyIdentity.Attributes.Append(document.CreateAttribute("culture")).Value = culture;

				XmlNode bindingRedirect = dependentAssembly.AppendChild(document.CreateElement("bindingRedirect", assemblyBindingConfigXmlNamespace));
				bindingRedirect.Attributes.Append(document.CreateAttribute("oldVersion")).Value = oldVersion;
				bindingRedirect.Attributes.Append(document.CreateAttribute("newVersion")).Value = newVersion;

				return true;
			}

			return false;
		}

		internal static bool RemoveBindingRedirect(XmlNamespaceManager ns, XmlNode assemblyBinding, string assemblyName)
		{
			XmlNode dependentAssembly = assemblyBinding.SelectSingleNode("net:dependentAssembly[net:assemblyIdentity/@name='" + assemblyName + "']", ns);
			
			if (dependentAssembly != null)
			{
				assemblyBinding.RemoveChild(dependentAssembly);
				return true;
			}

			return false;
		}

		public override void Install(System.Collections.IDictionary stateSaver)
		{
			// Call the base implementation first otherwise an exception may corrupt state and rollback will fail.  (Occurred in testing)
			base.Install(stateSaver);

			if (stateSaver == null)
				throw new ArgumentNullException("stateSaver");

			InstallEnvironmentVariables();

			if (InstallIntoVS2005 || InstallIntoVS2008)
			{
				IEnumerable<VisualStudioContent> contents = Contents;

				try
				{
					UpdateExternalUIConfiguration();
					
					ContentInstaller installer = CreateInstaller();

					installer.Install(contents);
				}
				catch (Exception ex)
				{
					Log.Exception(ex, "Installation error: ");

					if (ex is InstallException)
						throw;
					else
						throw new InstallException(string.Format(System.Globalization.CultureInfo.CurrentCulture, Resources.Errors.InstallError,
							Environment.NewLine, ex.Message), ex);
				}
				finally
				{
					// the contents should always be saved (in case of a rollback)
					stateSaver["contents"] = SerializeContents(contents);
				}
			}
		}

		public override void Rollback(System.Collections.IDictionary savedState)
		{
			base.Rollback(savedState);

			Uninstall(savedState, Resources.Text.Rollback, Resources.Errors.RollbackNoContentWarning, Resources.Errors.InstallRollbackError);
		}

		public override void Uninstall(System.Collections.IDictionary savedState)
		{
			base.Uninstall(savedState);

			Uninstall(savedState, Resources.Text.Uninstallation, Resources.Errors.UninstallNoContentWarning, Resources.Errors.InstallUninstallError);
		}

		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
		private void Uninstall(System.Collections.IDictionary savedState, string actionTitle, string missingContentWarning, string errorMessageFormat)
		{
			UninstallEnvironmentVariables();

			if (savedState == null || !savedState.Contains("contents"))
				return;

			AppDomain.CurrentDomain.AssemblyResolve += delegate(object sender, ResolveEventArgs e)
			// In testing, the executing assembly could not be resolved during deserialization of the VisualStudioContents[]
			// but this code solves the problem by simply returning the assembly.  It appears that Microsoft's custom action 
			// wrapper loads the assembly into the "From" context (i.e., Assembly.LoadFrom) instead of with Assembly.Load, which
			// is used by the binary formatter to resolve assemblies when deserializing.  This code allows a single assembly
			// to be shared by multiple load-contexts.
			{
				System.Reflection.Assembly assembly = typeof(CustomActions).Assembly;

				return (e.Name.Equals(assembly.FullName, StringComparison.Ordinal)) ? assembly : null;
			};

			try
			{
				IEnumerable<VisualStudioContent> contents = DeserializeContents(savedState["contents"] as string);

				if (contents != null)
				{
					ContentInstaller installer = CreateInstaller();
					installer.Uninstall(contents);
				}
				else
					Log.Warn(missingContentWarning);
			}
			catch (Exception ex)
			{
				try
				{
					Log.Exception(ex, actionTitle + " error: ");
				}
				finally
				{
					MessageBox.Show(string.Format(System.Globalization.CultureInfo.CurrentCulture, errorMessageFormat,
						Environment.NewLine, ex.Message), actionTitle + " Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
				}
			}
		}
		#endregion
	}
}